type Thenable<T> = T & { then(f: (p: T) => void): Promise<void> };

export function parseValue(stored: string | null): unknown {
  let value: unknown = void 0;
  const valid =
    typeof stored == "string" && stored.startsWith("[") && stored.endsWith("]");
  try {
    value = valid ? ((JSON.parse(stored) as [unknown])[0] ?? void 0) : void 0;
  } catch {
    value = void 0;
  }
  switch (value) {
    case "__$NaN": {
      value = Number.NaN;
      break;
    }
    case "__$UdF": {
      value = void 0;
      break;
    }
    case "__$FnT": {
      value = Infinity;
      break;
    }
    case "__$XnT": {
      value = -Infinity;
      break;
    }
  }
  return value;
}

const $polyfills = {
  GM_getValue: DM_getValue,
  parseValue,
};

let $listeners: [
  string,
  (key?: string, oldV?: unknown, newV?: unknown, remote?: boolean) => void,
][] = [];

export const $storListener = (storEvent: StorageEvent) => {
  const key = storEvent.key?.replace("$DMValue$DummyMonkeyScriptName$", "");
  for (const [id, listenerArray] of $listeners.entries()) {
    if (listenerArray[0] === key) {
      listenerArray[1].call(
        {
          id,
          key: key,
          cb: listenerArray[1],
        },
        key,
        $polyfills.parseValue(storEvent.oldValue),
        $polyfills.parseValue(storEvent.newValue),
        true,
      );
    }
  }
};

let $hasStorEventListener = false;

// Polyfill
export function DM_download(
  objectOrURL:
    | string
    | {
        url: string;
        name: string;
        onload?: () => void;
      },
  filename?: string,
) {
  const linkElement = document.createElement("a");
  if (typeof objectOrURL == "object") {
    linkElement.href = objectOrURL.url;
    linkElement.download = objectOrURL.name;
    if (objectOrURL.onload) {
      linkElement.addEventListener("click", objectOrURL.onload);
    }
  } else {
    linkElement.href = objectOrURL;
    linkElement.download = filename ?? "";
  }
  linkElement.style.cssText = "position:absolute;top:-100%";
  document.body.append(linkElement);
  setTimeout(() => {
    linkElement.click();
    linkElement.remove();
  }, 0);
  return {
    abort: () => void 0,
  };
}

export function DM_addElement<K extends keyof HTMLElementTagNameMap>(
  parentOrTagName: K | (Node | Element | ShadowRoot),
  tagNameOrObject?: K | Record<string, string | number | boolean>,
  object?: Record<string, string | number | boolean>,
) {
  let parent: Node | Element | ShadowRoot | undefined | null = void 0;
  let type = "div";
  let attributes: Record<string, string | number | boolean>;
  if (!!object && typeof parentOrTagName == "object") {
    parent = parentOrTagName;
    type = typeof tagNameOrObject == "string" ? tagNameOrObject : "div";
    attributes = object;
  } else {
    type = typeof parentOrTagName == "string" ? parentOrTagName : "div";
    attributes = typeof tagNameOrObject == "object" ? tagNameOrObject : {};
  }
  const element = document.createElement(type);
  for (const attribute of Object.getOwnPropertyNames(attributes)) {
    switch (attribute) {
      case "textContent": {
        element.textContent = attributes[attribute] as string;
        break;
      }
      case "value": {
        (element as HTMLInputElement).value = String(attributes[attribute]);
        break;
      }
      default: {
        element.setAttribute(attribute, String(attributes[attribute]));
      }
    }
  }
  function addElement() {
    parent ??= document.head;
    (parent as Element).append(element);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", addElement);
  } else {
    addElement();
  }
  (element as Thenable<HTMLElement>).then = <T,>(
    call: (element: HTMLElement) => T,
  ) => {
    return new Promise<T>((resolve) => {
      resolve(call(element));
    });
  };
  return element;
}

export function DM_setClipbooard(text: string) {
  const hideArea = document.createElement("textarea");
  hideArea.style.cssText = "position:absolute;top:-100%";
  hideArea.setAttribute("readonly", "");
  hideArea.value = text;
  document.body.append(hideArea);
  hideArea.select();
  document.execCommand("copy");
  hideArea.remove();
}

export function DM_addStyle(css: string) {
  const styleElement = document.createElement("style");
  styleElement.textContent = css;
  function addStyle() {
    const parent = document.head;
    parent.append(styleElement);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", addStyle);
  } else {
    addStyle();
  }
  return styleElement;
}

export function DM_openInTab(url: string) {
  const win = window.open(url);
  return win
    ? {
        close() {
          win.close();
        },
        focus() {
          win.focus();
        },
        get closed() {
          return win.closed;
        },
        set onclose(function_: (event_?: Event) => void) {
          win.addEventListener("unload", function_.bind(this));
        },
      }
    : void 0;
}

export function DM_notification(
  objectOrText: {
    text: string;
    title?: string;
    onclick?: () => void;
    ondone?: (clicked?: boolean) => void;
  },
  titleOrOndone?: (clicked?: boolean) => void,
): boolean;
export function DM_notification(
  objectOrText: string,
  titleOrOndone?: string,
  image?: string,
  onclick?: () => void,
): boolean;
export function DM_notification(
  this: unknown,
  objectOrText:
    | string
    | {
        text: string;
        title?: string;
        onclick?: () => void;
        ondone?: (clicked?: boolean) => void;
      },
  titleOrOndone?: string | ((clicked?: boolean) => void),
  _image?: string,
  onclick?: () => void,
): boolean {
  let notifyTitle = "";
  let notifyText = "";
  let notifyOnclick: (() => void) | undefined = void 0;
  let notifyOndone: ((clicked?: boolean) => void) | undefined = void 0;
  if (typeof objectOrText == "object") {
    notifyTitle = objectOrText.title ?? "脚本通知";
    notifyText = objectOrText.text;
    notifyOnclick = objectOrText.onclick;
    notifyOndone =
      typeof titleOrOndone == "function" ? titleOrOndone : objectOrText.ondone;
  } else {
    notifyTitle = typeof titleOrOndone == "string" ? titleOrOndone : "脚本通知";
    notifyText = objectOrText;
    notifyOnclick = onclick;
  }
  const ok = globalThis.confirm(
    `${notifyTitle ? notifyTitle + "\n\n" : ""}${notifyText}`,
  );
  if (ok) {
    notifyOnclick?.();
  }
  notifyOndone?.(ok);
  return ok; // 为了模拟 GM.notification 的 Promise<boolean>
}

export function DM_getValue<T>(key: string, defaultValue?: T): T | undefined {
  const stor = globalThis.localStorage.getItem(
    "$DMValue$DummyMonkeyScriptName$" + key,
  );
  return typeof stor == "string" && stor.startsWith("[") && stor.endsWith("]")
    ? ($polyfills.parseValue(stor) as T)
    : defaultValue;
}

export function DM_setValue(key: string, value: unknown) {
  const wrap = [];
  switch (typeof value) {
    case "function":
    case "bigint": {
      wrap[0] = value.toString();
      break;
    }
    case "undefined": {
      wrap[0] = "__$UdF";
      break;
    }
    case "number": {
      if (Number.isNaN(value)) {
        wrap[0] = "__$NaN";
      } else if (Number.isFinite(value)) {
        wrap[0] = value > 0 ? "__$FnT" : "__$XnT";
      } else {
        wrap[0] = value;
      }
      break;
    }
    default: {
      wrap[0] = value;
    }
  }
  const packed = JSON.stringify(wrap);
  for (const [id, listenerArray] of $listeners.entries()) {
    if (listenerArray[0] === key) {
      const oldValue = $polyfills.GM_getValue(key);
      listenerArray[1].call(
        {
          id,
          key,
          cb: listenerArray[1],
        },
        key,
        oldValue,
        value,
        false,
      );
    }
  }
  globalThis.localStorage.setItem(
    "$DMValue$DummyMonkeyScriptName$" + key,
    packed,
  );
}

export function DM_listValues() {
  const keysArray = [];
  for (let index = 0; index < globalThis.localStorage.length; index++) {
    const key = globalThis.localStorage.key(index);
    if (key?.startsWith("$DMValue$DummyMonkeyScriptName$")) {
      keysArray.push(key.replace("$DMValue$DummyMonkeyScriptName$", ""));
    }
  }
  return keysArray;
}

export function DM_deleteValue(key: string) {
  for (const [id, listenerArray] of $listeners.entries()) {
    if (listenerArray[0] === key) {
      const oldValue = $polyfills.GM_getValue(key);
      listenerArray[1].call(
        {
          id,
          key,
          cb: listenerArray[1],
        },
        key,
        oldValue,
        undefined,
        false,
      );
    }
  }
  globalThis.localStorage.removeItem("$DMValue$DummyMonkeyScriptName$" + key);
}

export function DM_addValueChangeListener(
  key: string,
  callback: (
    key?: string,
    oldV?: unknown,
    newV?: unknown,
    remote?: boolean,
  ) => void,
) {
  if (!$hasStorEventListener) {
    globalThis.addEventListener("storage", $storListener);
    $hasStorEventListener = true;
  }
  return $listeners.push([key, callback]) - 1;
}

export function DM_removeValueChangeListener(listenerId: number) {
  $listeners = $listeners.splice(listenerId, 1);
}
